package dev.ece.suit.loader.models.fbx;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import dev.ece.util.lib.Vector2f;
import dev.ece.util.lib.Vector3f;
import dev.ece.util.lib.Vector4f;

public class FBXGeometry {

	public final static Logger LOG = LogManager.getLogger(FBXGeometry.class);
	
	public final static String GEOMETRY = "Geometry";
	
	public final static String VERTICES = "Vertices";
	
	public final static String POLYGON_VERTEX_INDEX = "PolygonVertexIndex";
	
	public final static String LAYER_ELEMENT_MATERIAL = "LayerElementMaterial";
	
	public final static String MATERIALS = "Materials";
	
	public final static String MAPPING_INFORMATION_TYPE = "MappingInformationType";
	
	public final static String REFERENCE_INFORMATION_TYPE = "ReferenceInformationType";
	
	public final static String LAYER_ELEMENT_UV = "LayerElementUV";
	
	public final static String UV = "UV";
	
	public final static String UV_INDEX = "UVIndex";
	
	public final static String LAYER_ELEMENT_NORMAL = "LayerElementNormal";
	
	public final static String NORMALS = "Normals";
	
	public final static String NORMALS_INDEX = "NormalsIndex";
	
	/**
	 * 暂时不支持切线，以后用到法线贴图了再研究
	 */
	public final static String LAYER_ELEMENT_TANGENTS = "LayerElementTangents";
	
	
	private IndiceObject<Vector3f> vertice = new IndiceObject<Vector3f>();
	
	private IndiceObject<Vector4f> bones = new IndiceObject<Vector4f>();
	
	private IndiceObject<Vector4f> weights = new IndiceObject<Vector4f>();
	
	private IndiceObject<Vector2f> uv = new IndiceObject<Vector2f>();
	
	private IndiceObject<Vector3f> normals = new IndiceObject<Vector3f>();
	
	private int materialIndex = -1;
	
	private long id;
	
	private FBXMeshModel meshModel;
	
	public static FBXGeometry[] parseFBXGeometry(List<FBXElement> geometryElements) throws IOException {
		if(geometryElements != null && !geometryElements.isEmpty()) {
			List<FBXElement> list = findMeshGeometry(geometryElements);
			FBXGeometry[] geometries = new FBXGeometry[list.size()];
			for(int i=0; i<geometries.length; i++) {
				geometries[i] = parseFBXGeometry(list.get(i));
			}
			return geometries;
			}
		return null;
	}
	
	public static FBXGeometry parseFBXGeometry(FBXElement element) throws IOException {
		FBXGeometry geometry = new FBXGeometry();
		geometry.id = (Long) element.getProperties()[0].getValue();
		Map<String, List<FBXElement>> map = element.toFBXElementMap();
		//顶点
		FBXElement vertices = map.get(VERTICES).get(0);
		geometry.vertice.setObjects(vertices.getProperties()[0].toVector3f());
		LOG.debug("顶点数量：{}", geometry.vertice.getObjects().length);
		//多边形顶点索引
		FBXElement indice = map.get(POLYGON_VERTEX_INDEX).get(0);
		geometry.vertice.setIndice(indice.getProperties()[0].toIndice(true));
		LOG.debug("顶点索引数量：{}", geometry.vertice.getIndice().length);
		//纹理坐标，暂时只支持1层纹理贴图,如需混合贴图，需要改进
		FBXElement uvs = map.get(LAYER_ELEMENT_UV).get(0);
		geometry.uv.setObjects(uvs.findChildren(UV)[0].getProperties()[0].toVector2f());
		FBXElement[] uvIndice = uvs.findChildren(UV_INDEX);
		if(uvIndice.length > 0) {
			geometry.uv.setIndice(uvIndice[0].getProperties()[0].toIndice());
		}
		LOG.debug("纹理坐标数量：{}, 纹理索引数量：{}", geometry.uv.getObjects().length, geometry.uv.getIndice() != null ? geometry.uv.getIndice().length : 0);
		//法线
		FBXElement normals = map.get(LAYER_ELEMENT_NORMAL).get(0);
		geometry.normals.setObjects(normals.findChildren(NORMALS)[0].getProperties()[0].toVector3f());
		FBXElement[] normalIndice = normals.findChildren(NORMALS_INDEX);
		if(normalIndice.length > 0) {
			geometry.normals.setIndice(normalIndice[0].getProperties()[0].toIndice());
		}
		LOG.debug("法线数量：{}, 法线索引数量：{}", geometry.normals.getObjects().length, geometry.normals.getIndice() != null ? geometry.normals.getIndice().length : 0);
		geometry.initBoneWeights();
		return geometry;
	}
	
	public static List<FBXElement> findMeshGeometry(List<FBXElement> geometryElements) {
		List<FBXElement> list = new ArrayList<FBXElement>();
		for(FBXElement element: geometryElements) {
			if(element.hasProperties()) {
				FBXProperty property = element.getProperties()[element.getProperties().length - 1];
				if(property.getType() == 'S' && StringUtils.equals(FBXModelLoader.MESH, (String) property.getValue())) {
					list.add(element);
				}
			}
		}
		return list;
	}
	
	public static Map<Long, FBXGeometry> toFBXGeometryMap(Map<Long, List<FBXConnection>> connections, FBXGeometry[] geometries) {
		Map<Long, FBXGeometry> result = new ConcurrentHashMap<Long, FBXGeometry>();
		if(geometries != null) {
			List<FBXConnection> conns = null;
			for(FBXGeometry geometry: geometries) {
				conns = connections.get(geometry.getId());
				if(conns != null) {
					for(FBXConnection conn: conns) {
						result.put(geometry.getId(), geometry);
						result.put(conn.getTo(), geometry);
					}
				}
			}
		}
		return result;
	}
	
	public static Vector3f[] boundBox(FBXGeometry[] geometries) {
		Vector3f[] boxs = new Vector3f[2];
		Vector3f[] vertice = null;
		Vector3f box = null;
		for(FBXGeometry geometry: geometries) {
			vertice = geometry.getVertice().getObjects();
			if(boxs[0] == null) {
				boxs[0] = new Vector3f(vertice[0]);
				boxs[1] = new Vector3f(vertice[0]);
			}
			for(Vector3f vertex: vertice) {
				box = boxs[0];
				box.set(Math.min(vertex.getX(), box.getX()), 
						Math.min(vertex.getY(), box.getY()), 
						Math.min(vertex.getZ(), box.getZ()));
				box = boxs[1];
				box.set(Math.max(vertex.getX(), box.getX()), 
						Math.max(vertex.getY(), box.getY()), 
						Math.max(vertex.getZ(), box.getZ()));
			}
		}
		return boxs;
	}
	
	protected void fillBoneWeights(int jointIndex, FBXJoint joint) {
		int index = -1;
		float[] bones = null;
		float[] weights = null;
		for(int i=0; i<joint.getIndice().length; i++) {
			index = joint.getIndice()[i];
			bones = this.bones.getObjects()[index].getVector();
			weights = this.weights.getObjects()[index].getVector();
			for(int j=0; j<4; j++) {
				if(bones[j] < 0) {
					bones[j] = jointIndex;
					weights[j] = joint.getWeights()[i];
					break;
				}
			}
		}
	}
	
	protected void initBoneWeights() {
		Vector4f[] bones = new Vector4f[this.vertice.getObjects().length];
		Vector4f[] weights = new Vector4f[this.vertice.getObjects().length];
		for(int i=0; i<bones.length; i++) {
			bones[i] = new Vector4f(-1, -1, -1, -1);
			weights[i] = new Vector4f();
		}
		this.bones.setObjects(bones);
		this.weights.setObjects(weights);
		this.bones.setIndice(this.vertice.getIndice());
		this.weights.setIndice(this.vertice.getIndice());
	}
	
	public void fillFBXMesh(FBXMesh mesh) {
		int count = this.vertice.count();
		Vector3f vertex = null;
		for(int i=0; i<count; i++) {
			vertex = new Vector3f(this.vertice.get(i));
			getMeshModel().getGeometricMatrix().transform(vertex);
			mesh.getVertice().add(vertex);
			mesh.getBoneindice().add(this.bones.get(i));
			mesh.getWeights().add(this.weights.get(i));
			mesh.getTextureCoords().add(this.uv.get(i));
			mesh.getNormals().add(this.normals.get(i));
		}
	}
	
	public FBXMeshModel getMeshModel() {
		return meshModel;
	}

	protected void setMeshModel(FBXMeshModel meshModel) {
		this.meshModel = meshModel;
	}

	public IndiceObject<Vector3f> getVertice() {
		return vertice;
	}

	public IndiceObject<Vector2f> getUv() {
		return uv;
	}

	public IndiceObject<Vector3f> getNormals() {
		return normals;
	}

	public IndiceObject<Vector4f> getBones() {
		return bones;
	}

	public IndiceObject<Vector4f> getWeights() {
		return weights;
	}

	public int getMaterialIndex() {
		return materialIndex;
	}

	protected void setMaterialIndex(int materialIndex) {
		this.materialIndex = materialIndex;
	}

	public long getId() {
		return id;
	}

	public static class IndiceObject<T> {
		
		private T[] objects;
		
		private int[] indice;
		
		public int count() {
			return indice != null ? indice.length : objects.length;
		}
		public T get(int index) {
			return indice != null ? objects[indice[index]] : objects[index];
		}
		
		public T[] getObjects() {
			return objects;
		}
		public void setObjects(T[] objects) {
			this.objects = objects;
		}
		public int[] getIndice() {
			return indice;
		}
		public void setIndice(int[] indice) {
			this.indice = indice;
		}
		
	}
}

